Website Mirror Script 🌐

Website Mirror Script 🌐

这是一个Python 脚本,用于创建指定网站的本地离线镜像。它会从一个起始 URL 开始,递归地下载所有同域名下的页面、样式表、脚本和图片,并重写链接以便在本地进行浏览。

完成镜像后,脚本会自动启动一个本地 Web 服务器,让您可以方便地在浏览器中预览结果。

访问此项目的Github地址

功能特性 ✨

  • 递归下载: 从一个入口 URL 开始,自动抓取整个站点的结构和资源。
  • 链接重写: 智能地将页面中的绝对链接(如 href, src)转换为相对链接,确保镜像站点可以完全离线工作。
  • 资源处理: 能够下载 HTML、CSS、JavaScript 文件以及图片等多种资源类型。
  • 遵守 robots.txt: 可配置是否遵守目标网站的 robots.txt 爬虫协议,做一个“有礼貌的”爬虫。
  • 支持抓取延迟: 如果 robots.txt 中定义了 Crawl-delay,脚本会自动遵守,避免对服务器造成过大压力。
  • 内置 Web 服务器: 镜像完成后,一键启动本地服务器,立即在 http://localhost:PORT 查看效果。
  • 路径清理: 自动处理 URL 中的特殊字符,将其转换为安全的文件名和路径。

环境要求 📦

  • Python 3.x
  • 第三方库: requestsbeautifulsoup4

您可以使用 pip 轻松安装这些依赖:

1
pip install requests beautifulsoup4

文件说明 📄

  • requirements.txt: 列出项目依赖的 Python 库。
  • website-mirror.py: 主脚本文件,包含网站镜像的逻辑。

使用方法 🚀

  1. 安装依赖: 运行 pip install -r requirements.txt 安装所需的 Python 库。
  2. 配置脚本: 打开 website-mirror.py 文件,修改以下配置:
    • TARGET_URL: 替换为您想要镜像的网站 URL (例如: "https://www.example.com").
    • DOWNLOAD_DIR: 设置下载目录的名称 (默认为 "mirrored_site").
    • SERVER_PORT: 选择本地 Web 服务器使用的端口 (默认为 712).
    • RESPECT_ROBOTS_TXT: 设置为 True 以遵守 robots.txt 协议, 或 False 以忽略.
  3. 运行脚本: 在命令行中执行 python website-mirror.py
  4. 预览镜像: 脚本完成后, 会自动启动一个本地 Web 服务器. 在浏览器中访问 http://localhost:PORT (将 PORT 替换为您设置的端口号) 即可预览镜像的网站。

核心代码解析 🧩

让我们深入了解这个工具背后的核心代码 (website-mirror.py),重点分析两个核心函数:

  • download_and_process_url(url_to_fetch, base_domain, download_root)

    该函数负责下载指定 URL 的内容,并对 HTML 文件进行处理,提取链接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    def download_and_process_url(url_to_fetch, base_domain, download_root):
    """下载URL内容,如果是HTML则解析并查找更多链接。返回True表示成功,False表示失败。"""
    print(f"[*] 正在访问: {url_to_fetch}")

    try:
    response = session.get(url_to_fetch, timeout=15, stream=True) # Increased timeout
    response.raise_for_status()
    except requests.exceptions.RequestException as e:
    print(f"[!] 请求失败 {url_to_fetch}: {e}")
    visited_urls.add(url_to_fetch) # 标记为已访问,即使失败,以避免重试
    return False

    content_type = response.headers.get('content-type', '').lower()
    local_filepath, local_dir = get_local_path(base_domain, url_to_fetch, download_root)

    local_dir.mkdir(parents=True, exist_ok=True)

    with open(local_filepath, 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
    f.write(chunk)
    print(f" -> 已保存到: {local_filepath}")
    visited_urls.add(url_to_fetch) # 标记为已访问(成功下载后)

    if 'text/html' in content_type:
    encoding = response.encoding or 'utf-8'
    try:
    with open(local_filepath, 'r', encoding=encoding, errors='replace') as f_read:
    soup = BeautifulSoup(f_read.read(), 'html.parser')
    except Exception as e:
    print(f"[!] 解析HTML失败 {local_filepath}: {e}")
    return True # 文件已下载,但解析失败

    modified = False
    for tag_name, attr_name in [('a', 'href'), ('link', 'href'),
    ('img', 'src'), ('script', 'src')]:
    for tag in soup.find_all(tag_name):
    attr_value = tag.get(attr_name)
    if not attr_value:
    continue

    absolute_url = urljoin(url_to_fetch, attr_value)
    parsed_absolute_url = urlparse(absolute_url)

    if parsed_absolute_url.netloc == base_domain and \
    parsed_absolute_url.scheme in ['http', 'https']:

    target_local_path_obj, _ = get_local_path(base_domain, absolute_url, download_root)
    current_file_dir_obj = local_filepath.parent

    try:
    relative_new_path = os.path.relpath(target_local_path_obj, current_file_dir_obj)
    relative_new_path = relative_new_path.replace(os.sep, '/')

    if tag[attr_name] != relative_new_path:
    # print(f" 🔗 更新链接: {tag[attr_name]} -> {relative_new_path}")
    tag[attr_name] = relative_new_path
    modified = True
    except ValueError as ve:
    print(f"[!] 无法计算相对路径: {target_local_path_obj} from {current_file_dir_obj} - {ve}")

    if absolute_url not in visited_urls and absolute_url not in urls_to_visit:
    urls_to_visit.add(absolute_url)

    if modified:
    try:
    with open(local_filepath, 'w', encoding=encoding, errors='replace') as f_write:
    f_write.write(str(soup))
    print(f" -> 已更新链接并保存: {local_filepath}")
    except Exception as e:
    print(f"[!] 写入修改后的HTML失败 {local_filepath}: {e}")
    return True
    • 核心逻辑: 下载 URL 内容,如果是 HTML 则解析并查找更多链接,并将链接转换为相对路径。
    • 依赖: 使用 requests 库发送 HTTP 请求,使用 BeautifulSoup 库解析 HTML。
    • 链接转换: 使用 urljoin 将相对 URL 转换为绝对 URL,使用 os.path.relpath 计算相对路径,并更新 HTML 标签的属性值。
  • start_mirroring(target_url_str, download_dir_str)

    该函数是主镜像逻辑的入口点,负责递归地下载和处理 URL。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    def start_mirroring(target_url_str, download_dir_str):
    """主镜像逻辑"""
    download_root_path = Path(download_dir_str)
    if download_root_path.exists():
    print(f"[*] 清理旧的镜像目录: {download_dir_str}")
    shutil.rmtree(download_root_path)
    download_root_path.mkdir(parents=True, exist_ok=True)

    parsed_target_url = urlparse(target_url_str)
    base_domain = parsed_target_url.netloc
    user_agent_for_robots = session.headers.get("User-Agent", "*")

    if not base_domain:
    print(f"[!] 无效的目标URL,无法提取域名: {target_url_str}")
    return

    urls_to_visit.add(target_url_str)

    while urls_to_visit:
    current_url = urls_to_visit.pop()

    if current_url in visited_urls:
    continue

    current_domain = urlparse(current_url).netloc # Should always be base_domain

    if RESPECT_ROBOTS_TXT:
    rp = get_robot_parser_for_url(current_url)

    if not rp.can_fetch(user_agent_for_robots, current_url):
    print(f"[*] 跳过 (robots.txt禁止): {current_url}")
    visited_urls.add(current_url) # 标记为已访问,避免重试
    continue

    delay = rp.crawl_delay(user_agent_for_robots)
    if delay:
    last_request_time = last_request_times.get(current_domain, 0)
    wait_time = (last_request_time + delay) - time.time()
    if wait_time > 0:
    print(f" [i] 遵守Crawl-delay: 等待 {wait_time:.2f}s ({current_domain})")
    time.sleep(wait_time)

    download_successful = download_and_process_url(current_url, base_domain, download_root_path.resolve())

    if RESPECT_ROBOTS_TXT and download_successful: # 记录成功请求的时间
    last_request_times[current_domain] = time.time()

    # 可选:即使不遵守robots.txt,也加一个小延时
    # if not (RESPECT_ROBOTS_TXT and delay): # 如果没有 crawl-delay 控制
    # time.sleep(0.1) # 通用小延时

    print("\n[*] 镜像过程完成!")
    print(f"[*] 文件保存在: {download_root_path.resolve()}")
    • 核心逻辑: 使用 urls_to_visit 集合维护待下载的 URL 列表,使用 visited_urls 集合记录已下载的 URL,避免重复下载。 循环从 urls_to_visit 中取出一个 URL,调用 download_and_process_url 函数下载和处理内容。
    • robots.txt 支持: 如果 RESPECT_ROBOTS_TXTTrue,则先检查 robots.txt 文件,判断是否允许抓取该 URL。
    • 域名限制: 只下载与目标 URL 同域名的 URL。

其他重要函数

  • get_local_path(base_url_netloc, current_url_str, download_dir_base): 根据 URL 生成本地文件路径。
  • run_server(port, directory): 启动一个简单的 HTTP 服务器来查看镜像效果。

注意事项 ⚠️

  • robots.txt: 请尊重网站的 robots.txt 协议。不遵守 robots.txt 可能会导致您的 IP 被网站封禁。
  • 网站结构: 对于复杂的网站,镜像可能不完整。
  • 免责声明: 此脚本仅用于学习和研究目的。请勿用于非法用途。

总结 🎉

这个 Python 脚本提供了一个简单而强大的方法来创建网站的本地镜像。 它可以帮助您在没有网络连接的情况下浏览网站,或用于备份和归档网站内容。


声明: 本博客文章中如有任何涉及版权的内容,请立即联系 admin@main.712521.xyz,我们将尽快处理。